 /*******************************************************************************
  * Copyright (c) 2005, 2007 IBM Corporation and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
  * http://www.eclipse.org/legal/epl-v10.html
  *
  * Contributors:
  * IBM Corporation - initial API and implementation
  *******************************************************************************/
 package org.eclipse.ui.operations;

 import java.lang.reflect.InvocationTargetException ;

 import org.eclipse.core.commands.ExecutionException;
 import org.eclipse.core.commands.operations.IAdvancedUndoableOperation2;
 import org.eclipse.core.commands.operations.IOperationHistory;
 import org.eclipse.core.commands.operations.IOperationHistoryListener;
 import org.eclipse.core.commands.operations.IUndoContext;
 import org.eclipse.core.commands.operations.IUndoableOperation;
 import org.eclipse.core.commands.operations.OperationHistoryEvent;
 import org.eclipse.core.runtime.IAdaptable;
 import org.eclipse.core.runtime.IProgressMonitor;
 import org.eclipse.core.runtime.IStatus;
 import org.eclipse.core.runtime.OperationCanceledException;
 import org.eclipse.jface.action.Action;
 import org.eclipse.jface.operation.IRunnableWithProgress;
 import org.eclipse.osgi.util.NLS;
 import org.eclipse.swt.widgets.Display;
 import org.eclipse.swt.widgets.Shell;
 import org.eclipse.ui.IPartListener;
 import org.eclipse.ui.IWorkbenchPart;
 import org.eclipse.ui.IWorkbenchPartSite;
 import org.eclipse.ui.IWorkbenchWindow;
 import org.eclipse.ui.PlatformUI;
 import org.eclipse.ui.actions.ActionFactory;
 import org.eclipse.ui.internal.WorkbenchMessages;
 import org.eclipse.ui.internal.WorkbenchPlugin;
 import org.eclipse.ui.internal.misc.StatusUtil;
 import org.eclipse.ui.internal.operations.TimeTriggeredProgressMonitorDialog;
 import org.eclipse.ui.internal.util.Util;
 import org.eclipse.ui.part.MultiPageEditorSite;
 import org.eclipse.ui.statushandlers.StatusManager;

 /**
  * <p>
  * OperationHistoryActionHandler implements common behavior for the undo and
  * redo actions. It supports filtering of undo or redo on a particular undo
  * context. If an undo context is not specified, or there has been no history
  * available for the specified undo context, then the workbench undo context
  * will be used.
  * </p>
  * <p>
  * OperationHistoryActionHandler provides an adapter in the info parameter of
  * the IOperationHistory undo and redo methods that is used to get UI info for
  * prompting the user during operations or operation approval. Adapters are
  * provided for org.eclipse.ui.IWorkbenchWindow, org.eclipse.swt.widgets.Shell,
  * org.eclipse.ui.IWorkbenchPart, org.eclipse.core.commands.IUndoContext, and
  * org.eclipse.runtime.IProgressMonitor.
  * </p>
  * <p>
  * OperationHistoryActionHandler assumes a linear undo/redo model. When the
  * handler is run, the operation history is asked to perform the most recent
  * undo/redo for the handler's undo context. The handler can be configured
  * (using #setPruneHistory(true)) to flush the operation undo or redo history
  * for the handler's undo context when there is no valid operation on top of the
  * history. This avoids keeping a stale history of invalid operations. By
  * default, pruning does not occur and it is assumed that clients of the
  * particular undo context are pruning the history when necessary.
  * </p>
  *
  * @since 3.1
  */
 public abstract class OperationHistoryActionHandler extends Action implements
         ActionFactory.IWorkbenchAction, IAdaptable {

     private static final int MAX_LABEL_LENGTH = 32;

     private class PartListener implements IPartListener {
         /**
          * @see IPartListener#partActivated(IWorkbenchPart)
          */
         public void partActivated(IWorkbenchPart part) {
         }

         /**
          * @see IPartListener#partBroughtToTop(IWorkbenchPart)
          */
         public void partBroughtToTop(IWorkbenchPart part) {
         }

         /**
          * @see IPartListener#partClosed(IWorkbenchPart)
          */
         public void partClosed(IWorkbenchPart part) {
             if (part.equals(site.getPart())) {
                 dispose();
                 // Special case for MultiPageEditorSite
 // See https://bugs.eclipse.org/bugs/show_bug.cgi?id=103379
 } else if ((site instanceof MultiPageEditorSite)
                     && (part.equals(((MultiPageEditorSite) site)
                             .getMultiPageEditor()))) {
                 dispose();
             }
         }

         /**
          * @see IPartListener#partDeactivated(IWorkbenchPart)
          */
         public void partDeactivated(IWorkbenchPart part) {
         }

         /**
          * @see IPartListener#partOpened(IWorkbenchPart)
          */
         public void partOpened(IWorkbenchPart part) {
         }

     }

     private class HistoryListener implements IOperationHistoryListener {
         public void historyNotification(final OperationHistoryEvent event) {
             Display display = getWorkbenchWindow().getWorkbench().getDisplay();
             switch (event.getEventType()) {
             case OperationHistoryEvent.OPERATION_ADDED:
             case OperationHistoryEvent.OPERATION_REMOVED:
             case OperationHistoryEvent.UNDONE:
             case OperationHistoryEvent.REDONE:
                 if (display != null
                         && event.getOperation().hasContext(undoContext)) {
                     contextActive = true;
                     display.asyncExec(new Runnable () {
                         public void run() {
                             update();
                         }
                     });
                 }
                 break;
             case OperationHistoryEvent.OPERATION_NOT_OK:
                 if (display != null
                         && event.getOperation().hasContext(undoContext)) {
                     contextActive = true;
                     display.asyncExec(new Runnable () {
                         public void run() {
                             if (pruning) {
                                 IStatus status = event.getStatus();
                                 /*
                                  * Prune the history unless we can determine
                                  * that this was a cancelled attempt. See
                                  * https://bugs.eclipse.org/bugs/show_bug.cgi?id=101215
                                  */
                                 if (status == null
                                         || status.getSeverity() != IStatus.CANCEL) {
                                     flush();
                                 }
                                 // not all flushes will trigger an update so
 // force it here
 update();
                             } else {
                                 update();
                             }
                         }
                     });
                 }
                 break;
             case OperationHistoryEvent.OPERATION_CHANGED:
                 if (event.getOperation().hasContext(undoContext)) {
                     contextActive = true;
                 }
                 if (display != null && event.getOperation() == getOperation()) {
                     display.asyncExec(new Runnable () {
                         public void run() {
                             update();
                         }
                     });
                 }
                 break;
             default:
                 if (event.getOperation().hasContext(undoContext)) {
                     contextActive = true;
                 }
                 break;
             }
         }
     }

     private boolean contextActive = false;

     private boolean pruning = false;

     private IPartListener partListener = new PartListener();

     private IOperationHistoryListener historyListener = new HistoryListener();

     private TimeTriggeredProgressMonitorDialog progressDialog;

     private IUndoContext undoContext = null;

     IWorkbenchPartSite site;

     /**
      * Construct an operation history action for the specified workbench window
      * with the specified undo context.
      *
      * @param site -
      * the workbench part site for the action.
      * @param context -
      * the undo context to be used
      */
     OperationHistoryActionHandler(IWorkbenchPartSite site, IUndoContext context) {
         // string will be reset inside action
 super(""); //$NON-NLS-1$
 this.site = site;
         undoContext = context;
         checkUndoContext();
         site.getPage().addPartListener(partListener);
         getHistory().addOperationHistoryListener(historyListener);
         // An update must be forced in case the undo limit is 0.
 // see bug #89707
 update();
     }

     /*
      * (non-Javadoc)
      *
      * @see org.eclipse.ui.actions.ActionFactory.IWorkbenchAction#dispose()
      */
     public void dispose() {

         IOperationHistory history = getHistory();
         if (history != null) {
             history.removeOperationHistoryListener(historyListener);
         }

         if (isInvalid()) {
             return;
         }

         site.getPage().removePartListener(partListener);
         site = null;
         progressDialog = null;
         // We do not flush the history for our undo context because it may be
 // used elsewhere. It is up to clients to clean up the history
 // appropriately.
 // We do null out the context to signify that this handler is no longer
 // accessing the history.
 undoContext = null;
     }

     /*
      * Flush the history associated with this action.
      */
     abstract void flush();

     /*
      * Return the string describing the command, including the binding for the
      * operation label.
      */
     abstract String getCommandString();

     /*
      * Return the string describing the command for a tooltip, including the
      * binding for the operation label.
      */
     abstract String getTooltipString();

     /*
      * Return the simple string describing the command, with no binding to any
      * operation.
      */
     abstract String getSimpleCommandString();

     /*
      * Return the simple string describing the tooltip, with no binding to any
      * operation.
      */
     abstract String getSimpleTooltipString();

     /*
      * Return the operation history we are using.
      */
     IOperationHistory getHistory() {
         if (PlatformUI.getWorkbench() == null) {
             return null;
         }

         return PlatformUI.getWorkbench().getOperationSupport()
                 .getOperationHistory();
     }

     /*
      * Return the current operation.
      */
     abstract IUndoableOperation getOperation();

     /*
      * (non-Javadoc)
      *
      * @see org.eclipse.ui.actions.ActionFactory.IWorkbenchAction#run()
      */
     public final void run() {
         if (isInvalid()) {
             return;
         }

         Shell parent = getWorkbenchWindow().getShell();
         progressDialog = new TimeTriggeredProgressMonitorDialog(parent,
                 getWorkbenchWindow().getWorkbench().getProgressService()
                         .getLongOperationTime());
         IRunnableWithProgress runnable = new IRunnableWithProgress() {
             public void run(IProgressMonitor pm)
                     throws InvocationTargetException {
                 try {
                     runCommand(pm);
                 } catch (ExecutionException e) {
                     if (pruning) {
                         flush();
                     }
                     throw new InvocationTargetException (e);
                 }
             }
         };
         try {
             boolean runInBackground = false;
             if (getOperation() instanceof IAdvancedUndoableOperation2) {
                 runInBackground = ((IAdvancedUndoableOperation2) getOperation())
                         .runInBackground();
             }
             progressDialog.run(runInBackground, true, runnable);
         } catch (InvocationTargetException e) {
             Throwable t = e.getTargetException();
             if (t == null) {
                 reportException(e);
             } else {
                 reportException(t);
             }
         } catch (InterruptedException e) {
             // Operation was cancelled and acknowledged by runnable with this
 // exception.
 // Do nothing.
 } catch (OperationCanceledException e) {
             // the operation was cancelled. Do nothing.
 } finally {
             progressDialog = null;
         }
     }

     abstract IStatus runCommand(IProgressMonitor pm) throws ExecutionException;

     /*
      * (non-Javadoc)
      *
      * @see org.eclipse.core.runtime.IAdaptable#getAdapter(java.lang.Class)
      */
     public Object getAdapter(Class adapter) {
         if (adapter.equals(IUndoContext.class)) {
             return undoContext;
         }
         if (adapter.equals(IProgressMonitor.class)) {
             if (progressDialog != null) {
                 return progressDialog.getProgressMonitor();
             }
         }
         if (site != null) {
             if (adapter.equals(Shell.class)) {
                 return getWorkbenchWindow().getShell();
             }
             if (adapter.equals(IWorkbenchWindow.class)) {
                 return getWorkbenchWindow();
             }
             if (adapter.equals(IWorkbenchPart.class)) {
                 return site.getPart();
             }
             // Refer all other requests to the part itself.
 // See https://bugs.eclipse.org/bugs/show_bug.cgi?id=108144
 IWorkbenchPart part = site.getPart();
             if (part != null) {
                 return Util.getAdapter(part, adapter);
             }
         }
         return null;
     }

     /*
      * Return the workbench window for this action handler
      */
     private IWorkbenchWindow getWorkbenchWindow() {
         if (site != null) {
             return site.getWorkbenchWindow();
         }
         return null;
     }

     /**
      * The undo and redo subclasses should implement this.
      *
      * @return - a boolean indicating enablement state
      */
     abstract boolean shouldBeEnabled();

     /**
      * Set the context shown by the handler. Normally the context is set up when
      * the action handler is created, but the context can also be changed
      * dynamically.
      *
      * @param context
      * the context to be used for the undo history
      */
     public void setContext(IUndoContext context) {
         // optimization - do nothing if there was no real change
 if (context == undoContext) {
             return;
         }
         undoContext = context;
         checkUndoContext();
         update();
     }

     /**
      * Specify whether the action handler should actively prune the operation
      * history when invalid operations are encountered. The default value is
      * <code>false</code>.
      *
      * @param prune
      * <code>true</code> if the history should be pruned by the
      * handler, and <code>false</code> if it should not.
      *
      */
     public void setPruneHistory(boolean prune) {
         pruning = prune;
     }

     /**
      * Update enabling and labels according to the current status of the
      * operation history.
      */
     public void update() {
         if (isInvalid()) {
             return;
         }

         boolean enabled = shouldBeEnabled();
         String text, tooltipText;
         if (enabled) {
             tooltipText = NLS.bind(getTooltipString(), getOperation()
                     .getLabel());
             text = NLS.bind(getCommandString(), shortenText(getOperation()
                     .getLabel()));
         } else {
             tooltipText = NLS.bind(
                     WorkbenchMessages.Operations_undoRedoCommandDisabled,
                     getSimpleTooltipString());
             text = getSimpleCommandString();
             /*
              * if there is nothing to do and we are pruning the history, flush
              * the history of this context.
              */
             if (undoContext != null && pruning) {
                 flush();
             }
         }
         setText(text);
         setToolTipText(tooltipText);
         setEnabled(enabled);
     }

     /*
      * Shorten the specified command label if it is too long
      */
     private String shortenText(String message) {
         int length = message.length();
         if (length > MAX_LABEL_LENGTH) {
             StringBuffer result = new StringBuffer ();
             int mid = MAX_LABEL_LENGTH / 2;
             result.append(message.substring(0, mid));
             result.append("..."); //$NON-NLS-1$
 result.append(message.substring(length - mid));
             return result.toString();
         }
         return message;
     }

     /*
      * Report the specified exception to the log and to the user.
      */
     final void reportException(Throwable t) {
         // get any nested exceptions
 Throwable nestedException = StatusUtil.getCause(t);
         Throwable exception = (nestedException == null) ? t : nestedException;

         // Messages
 String exceptionMessage = exception.getMessage();
         if (exceptionMessage == null) {
             exceptionMessage = WorkbenchMessages.WorkbenchWindow_exceptionMessage;
         }
         IStatus status = StatusUtil.newStatus(WorkbenchPlugin.PI_WORKBENCH,
                 exceptionMessage, exception);

         // Log and show the problem
 WorkbenchPlugin.log(exceptionMessage, status);
         StatusUtil.handleStatus(status, StatusManager.SHOW,
                 getWorkbenchWindow().getShell());
     }

     /*
      * Answer true if the receiver is not valid for running commands, accessing
      * the history, etc.
      */
     final boolean isInvalid() {
         return undoContext == null || site == null;
     }

     /*
      * Get the undo context that should be used.
      */
     final IUndoContext getUndoContext() {
         // If no context was specified, or the specified one is not
 // in use, use the workbench context.
 if (undoContext == null || !contextActive) {
             return PlatformUI.getWorkbench().getOperationSupport()
                     .getUndoContext();
         }
         return undoContext;
     }

     /*
      * The undo context has been set. Check whether there is undo or redo
      * history available and set the contextActive flag accordingly. We check
      * both undo and redo here because we don't ever want the undo/redo action
      * handlers to have different values for contextActive.
      */
     private void checkUndoContext() {
         if (undoContext == null) {
             return;
         }
         contextActive = getHistory().getUndoOperation(undoContext) != null
                 || getHistory().getRedoOperation(undoContext) != null;
     }
 }

